﻿using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration;
using System.Data.Entity.ModelConfiguration.Configuration;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace DomainDriver.DomainModeling.StorageManagers.EntityFrameworkStorage
{
    public static class ForcedMappingUtilities
    {
        public const int StructMethodIndex = 13;
        public const int NullableStructMethodIndex = 14;
        public const int StringMethodIndex = 15;
        public const string PropertyMethodName = "Property";
        public const string LamdaArgumentName = "entity";

        public static void CreateForcedMappings(DbContext dbContext, DbModelBuilder modelBuilder)
        {
            Assembly efAssembly = Assembly.GetAssembly(typeof(DbContext));
            AssemblyName efName = new AssemblyName(efAssembly.FullName);

            if ((efName.Version.Major < 4) || ((efName.Version.Major == 4) && (efName.Version.Minor <= 2)))
            {
                CreateForcedMappingsForPreVersion4Dot2(dbContext, modelBuilder);
            }
            else if (efName.Version.Major < 6)
            {
                CreateForcedMappingsForVersion4Dot2To5(dbContext, modelBuilder);
            }
            else
            {
                CreateForceMappingsForPostVersion6(dbContext, modelBuilder);
            }
        }

        public static void CreateForceMappingsForPostVersion6(DbContext dbContext, DbModelBuilder modelBuilder)
        {
            var forceMappedConvention = new ForceMappedConvention();
            modelBuilder.Conventions.Add(forceMappedConvention);
        }

        public static void CreateForcedMappingsForVersion4Dot2To5(DbContext dbContext, DbModelBuilder modelBuilder)
        {
            Type contextType = dbContext.GetType();
            Type modelBuilderType = modelBuilder.GetType();
            Type genericDbSetType = typeof(DbSet<>);
            MethodInfo genericGetEntityInfo = modelBuilderType.GetMethod("Entity");

            foreach (PropertyInfo contextPropertyInfo in contextType.GetProperties())
            {
                MethodInfo methodInfo = contextPropertyInfo.GetGetMethod();
                Type returnType = methodInfo.ReturnType;

                if (!returnType.IsGenericType)
                { continue; }

                Type genericTypeDef = returnType.GetGenericTypeDefinition();

                if (!genericTypeDef.Equals(genericDbSetType))
                { continue; }

                Type entityType = methodInfo.ReturnType.GetGenericArguments().First();

                foreach (PropertyInfo entityPropertyInfo in entityType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
                {
                    Type propertyType = entityPropertyInfo.PropertyType;
                    bool implementsForceMappedAttribute = entityPropertyInfo.IsDefined(typeof(ForceMappedAttribute), true);

                    if (!implementsForceMappedAttribute)
                    { continue; }

                    MethodInfo typedGetEntityInfo = genericGetEntityInfo.MakeGenericMethod(new Type[] { entityType });
                    object configManager = typedGetEntityInfo.Invoke(modelBuilder, new object[] { });
                    MethodInfo nonGenericPropertyMapMethodInfo = null;

                    nonGenericPropertyMapMethodInfo = GetPropertyMappingMethodForPostVersion4Dot2(propertyType, configManager);

                    ParameterExpression pe = Expression.Parameter(entityType, LamdaArgumentName);
                    MemberExpression me = Expression.Property(pe, entityPropertyInfo.Name);
                    LambdaExpression le = Expression.Lambda(me, new[] { pe });
                    nonGenericPropertyMapMethodInfo.Invoke(configManager, new object[] { le });
                }
            }
        }

        private static MethodInfo GetPropertyMappingMethodForPostVersion4Dot2(Type propertyType, object configManager)
        {
            MethodInfo nonGenericPropertyMapMethodInfo = null;

            if (propertyType.IsValueType && !IsNullableType(propertyType))
            {
                //MethodInfo genericPropertyMapMethodInfo = configManager.GetType().GetMethods()[StructMethodIndex];
                MethodInfo genericPropertyMapMethodInfo = null;
                foreach (MethodInfo methodInfo in configManager.GetType().GetMethods().Where((m) => (m.Name == PropertyMethodName)))
                {
                    string argName = methodInfo.GetParameters()[0].ToString();
                    if (argName.Contains("Nullable`1[T]"))
                    { continue; }
                    if (!argName.Contains(",T"))
                    { continue; }

                    genericPropertyMapMethodInfo = methodInfo;
                    break;
                }

                nonGenericPropertyMapMethodInfo = genericPropertyMapMethodInfo.MakeGenericMethod(new Type[] { propertyType });
            }
            else if (propertyType.IsValueType && IsNullableType(propertyType))
            {
                //MethodInfo genericPropertyMapMethodInfo = configManager.GetType().GetMethods()[NullableStructMethodIndex];
                MethodInfo genericPropertyMapMethodInfo = null;
                foreach (MethodInfo methodInfo in configManager.GetType().GetMethods().Where((m) => (m.Name == PropertyMethodName)))
                {
                    string argName = methodInfo.GetParameters()[0].ToString();
                    if (!argName.Contains("Nullable`1[T]"))
                    { continue; }

                    genericPropertyMapMethodInfo = methodInfo;
                    break;
                }

                Type nonGenericPropertyType = propertyType.GetGenericArguments()[0];
                nonGenericPropertyMapMethodInfo = genericPropertyMapMethodInfo.MakeGenericMethod(new Type[] { nonGenericPropertyType });
            }
            else if (propertyType == typeof(string))
            {
                //nonGenericPropertyMapMethodInfo = configManager.GetType().GetMethods()[StringMethodIndex];
                foreach (MethodInfo methodInfo in configManager.GetType().GetMethods().Where((m) => (m.Name == PropertyMethodName)))
                {
                    string argName = methodInfo.GetParameters()[0].ToString();
                    if (!argName.Contains("String"))
                    { continue; }

                    nonGenericPropertyMapMethodInfo = methodInfo;
                    break;
                }
            }
            else if (propertyType == typeof(byte[]))
            {
                foreach (MethodInfo methodInfo in configManager.GetType().GetMethods().Where((m) => (m.Name == PropertyMethodName)))
                {
                    string argName = methodInfo.GetParameters()[0].ToString();
                    if (!argName.ToLower().Contains("byte[]"))
                    { continue; }

                    nonGenericPropertyMapMethodInfo = methodInfo;
                    break;
                }
            }
            else
            { throw new InvalidOperationException("Invalid force mapping type encountered."); }

            return nonGenericPropertyMapMethodInfo;
        }

        public static void CreateForcedMappingsForPreVersion4Dot2(DbContext dbContext, DbModelBuilder modelBuilder)
        {
            Type contextType = dbContext.GetType();
            Type genericDbSetType = typeof(DbSet<>);

            foreach (PropertyInfo propInfo in contextType.GetProperties())
            {
                MethodInfo methodInfo = propInfo.GetGetMethod();
                Type returnType = methodInfo.ReturnType;

                if (!returnType.IsGenericType)
                { continue; }

                Type genericTypeDef = returnType.GetGenericTypeDefinition();

                if (!genericTypeDef.Equals(genericDbSetType))
                { continue; }

                Type entityType = methodInfo.ReturnType.GetGenericArguments().First();

                Type builderType = typeof(DbModelBuilder);
                MethodInfo genericEntityMethod = builderType.GetMethods().Where((MethodInfo mi) => (mi.Name == "Entity")).First();
                MethodInfo actualEntityMethod = genericEntityMethod.MakeGenericMethod(new[] { entityType });
                object mapper = actualEntityMethod.Invoke(modelBuilder, null);

                Type utilitiesType = typeof(ForcedMappingUtilities);
                MethodInfo genericMapMethod = utilitiesType.GetMethods().Where((MethodInfo mi) => (mi.Name == "CreateForcedMappingsForEntityType")).First();
                MethodInfo actualMapMethod = genericMapMethod.MakeGenericMethod(new[] { entityType });
                object result = actualMapMethod.Invoke(utilitiesType, new object[] { mapper });
            }
        }

        public static void CreateForcedMappingsForEntityType<ET>(EntityTypeConfiguration<ET> mapper)
            where ET : class
        {
            Type entityType = typeof(ET);

            foreach (PropertyInfo propInfo in entityType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
            {
                Type propertyType = propInfo.PropertyType;
                bool implementsForceMappedAttribute = propInfo.IsDefined(typeof(ForceMappedAttribute), true);

                if (!implementsForceMappedAttribute)
                { continue; }

                if (propertyType == typeof(string))
                {
                    mapper.MapNonPublicStringProperty<ET>(propInfo.Name);
                }
                else if (propertyType == typeof(byte[]))
                {
                    mapper.MapNonPublicByteArrayProperty<ET>(propInfo.Name);
                }
                else if (propertyType.IsValueType && IsNullableType(propertyType))
                {
                    Type utilitiesType = typeof(ForcedMappingUtilities);
                    MethodInfo genericMapMethod = utilitiesType.GetMethods().Where((MethodInfo mi) => (mi.Name == "MapNonPublicNullableStructProperty")).First();

                    Type actualPropertyType = propertyType.GetGenericArguments()[0];
                    MethodInfo actualMapMethod = genericMapMethod.MakeGenericMethod(new[] { typeof(ET), actualPropertyType });
                    object result = actualMapMethod.Invoke(utilitiesType, new object[] { mapper, propInfo.Name });
                }
                else if (propertyType.IsValueType)
                {
                    Type utilitiesType = typeof(ForcedMappingUtilities);
                    MethodInfo genericMapMethod = utilitiesType.GetMethods().Where((MethodInfo mi) => (mi.Name == "MapNonPublicStructProperty")).First();
                    MethodInfo actualMapMethod = genericMapMethod.MakeGenericMethod(new[] { typeof(ET), propertyType });
                    object result = actualMapMethod.Invoke(utilitiesType, new object[] { mapper, propInfo.Name });
                }
                else
                { throw new InvalidOperationException("Property type is not supported."); }
            }
        }

        public static bool IsNullableType(Type valueType)
        {
            // LATER: Consider trying "(valueType.GetGenericTypeDefinition() == typeof(Nullable<>))"
            Type nullableType = typeof(Nullable);
            return ((valueType.IsGenericType) && (valueType.FullName.Contains("Nullable")));
        }

        public static void MapNonPublicStructProperty<ENTITY_TYPE, PROP_TYPE>(this EntityTypeConfiguration<ENTITY_TYPE> mapper, String propertyName)
            where ENTITY_TYPE : class
            where PROP_TYPE : struct
        {
            Type entityType = typeof(ENTITY_TYPE);
            ParameterExpression arg = Expression.Parameter(entityType, "x");
            Expression expr = arg;

            PropertyInfo pi = entityType.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            expr = Expression.Property(expr, pi);

            LambdaExpression lambda = Expression.Lambda(expr, arg);
            Expression<Func<ENTITY_TYPE, PROP_TYPE>> expression = (Expression<Func<ENTITY_TYPE, PROP_TYPE>>)lambda;

            PrimitivePropertyConfiguration propertyConfig = mapper.Property(expression);
            propertyConfig.HasColumnName(propertyName);
        }

        public static void MapNonPublicNullableStructProperty<ENTITY_TYPE, PROP_TYPE>(this EntityTypeConfiguration<ENTITY_TYPE> mapper, String propertyName)
            where ENTITY_TYPE : class
            where PROP_TYPE : struct
        {
            Type entityType = typeof(ENTITY_TYPE);
            ParameterExpression arg = Expression.Parameter(entityType, "x");
            Expression expr = arg;

            PropertyInfo pi = entityType.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            expr = Expression.Property(expr, pi);

            LambdaExpression lambda = Expression.Lambda(expr, arg);
            Expression<Func<ENTITY_TYPE, Nullable<PROP_TYPE>>> expression = (Expression<Func<ENTITY_TYPE, Nullable<PROP_TYPE>>>)lambda;

            PrimitivePropertyConfiguration propertyConfig = mapper.Property(expression);
            propertyConfig.HasColumnName(propertyName);
        }

        public static void MapNonPublicStringProperty<ENTITY_TYPE>(this EntityTypeConfiguration<ENTITY_TYPE> mapper, String propertyName)
            where ENTITY_TYPE : class
        {
            Type entityType = typeof(ENTITY_TYPE);
            ParameterExpression arg = Expression.Parameter(entityType, "x");
            Expression expr = arg;

            PropertyInfo pi = entityType.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            expr = Expression.Property(expr, pi);

            LambdaExpression lambda = Expression.Lambda(expr, arg);
            Expression<Func<ENTITY_TYPE, string>> expression = (Expression<Func<ENTITY_TYPE, string>>)lambda;

            PrimitivePropertyConfiguration propertyConfig = mapper.Property(expression);
            propertyConfig.HasColumnName(propertyName);
        }

        public static void MapNonPublicByteArrayProperty<ENTITY_TYPE>(this EntityTypeConfiguration<ENTITY_TYPE> mapper, String propertyName)
            where ENTITY_TYPE : class
        {
            Type entityType = typeof(ENTITY_TYPE);
            ParameterExpression arg = Expression.Parameter(entityType, "x");
            Expression expr = arg;

            PropertyInfo pi = entityType.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            expr = Expression.Property(expr, pi);

            LambdaExpression lambda = Expression.Lambda(expr, arg);
            Expression<Func<ENTITY_TYPE, byte[]>> expression = (Expression<Func<ENTITY_TYPE, byte[]>>)lambda;

            PrimitivePropertyConfiguration propertyConfig = mapper.Property(expression);
            propertyConfig.HasColumnName(propertyName);
        }
    }
}